#!/usr/bin/ruby
#
# Copyright:: Copyright 2012, Trimble Navigation Limited
# License:: All Rights Reserved.
# Original Author:: Scott Lininger (mailto:scott@sketchup.com)
#
# This file creates an instance of DCObservers class, which contains the
# following observers that monitor SketchUp for Dynamic Component interactions:
#
#     DCAppObserver  Watches SketchUp to attach other observers
#     DCToolsObserver  Watches the scale and move tool
#     DCSelectionObserver  Watches the selection to update WebDialogs
#     DCDefinitionsObserver  Watches for additions to definition list
#     DCModelObserver  Watches for various model level events
#     DCAppObserver  Watches for opening/closing of files

require "sketchup.rb"
require "langhandler.rb"

# The actual class plus the tools that are available are stored in an external
# file. This list of files could grow as new versions of the plugin are created.
Sketchup::require "su_dynamiccomponents/ruby/dcclass_v1"

# A temporary file of reworked functions from dcclass_v1.rb which helps work
# move forward in parallel until dcclass is refactored/split up.
Sketchup::require "su_dynamiccomponents/ruby/dcclass_overlays"

# Pull in the interact tool.
Sketchup::require "su_dynamiccomponents/ruby/dctools"

# Pull in some supporting classes.
Sketchup::require "su_dynamiccomponents/ruby/dcutils"
Sketchup::require "su_dynamiccomponents/ruby/dcconverter"
Sketchup::require "su_dynamiccomponents/ruby/generatereport"

class DCObservers

  # This is the maximum number of selected elements that we will bother parsing
  # to determine whether to show a context menu.
  MAX_SELECTION_LENGTH = 500

  # Populates an internal hash structure recording all known versions of the
  # dynamic components plugin with pointers out to the class that will handle
  # interactions for dynamic components of that version.
  #
  # For example, if in the future there are three different versions of the
  # plugin that are released, the following code will be present in
  # the initialize() method
  #
  #   @dc1 = DynamicComponentsV1.new()
  #   @dc2 = DynamicComponentsV2.new()
  #   @dc3 = DynamicComponentsV2p5.new()
  #   @known_versions = {}
  #   @known_versions[1.0] = @dc1
  #   @known_versions[2.0] = @dc2
  #   @known_versions[3.0] = @dc3
  #   Sketchup.add_observer( DCObservers::DCAppObserver.new() )
  #
  # This hash is passed into DCAppObserver which will farm it out to all of the
  # other observers. Each observer is then responsible for sending calls to the
  # appropriate class for each component it touches.
  def initialize()
    # puts "DCObservers initialize   ……ing👇"
    $dc_CONFIGURATOR_NAME = translate("Component Options")
    $dc_MANAGER_NAME = translate("Component Attributes")
    $dc_REPORTER_NAME = translate("Generate Report")

    # Create instance variables for each dc class.
    # (For now, there is only one, but in the future we may grow this list)
    @dc1 = DynamicComponentsV1.new(self)

    # Create and populate a hash pointing to the appropriate class for
    # each version that the plugin knows about. (v0.0 was for early testing,
    # but we'll leave it in the stack just in case some ancient components
    # are floating around.)
    @known_versions = {}
    @known_versions[0.0] = @dc1
    @known_versions[1.0] = @dc1

    # This Hash will contain keys of entityIDs, with a value of true, for
    # any dynamic components where we have shown the version alert box for.
    @has_shown_version_error = {}

    # Figure out the latest version that is supported inside our hash, and
    # store that value in @latest_version. If a component is edited and it does
    # not have an explicit version, then it will be handled using this latest
    # version. (In other words, new dynamic components will always be created
    # using the latest version.)
    @latest_version = 0.0
    for version_number in @known_versions.keys
      if version_number > @latest_version
        @latest_version = version_number
        @latest_class = @known_versions[version_number]
      end
    end

    # Attach our app observer that will create and manage all other observers
    Sketchup.add_observer(DCAppObserver.new(self))

    unless file_loaded?(__FILE__)
      # Add our UI hooks.
      # Create our menu items.
      window_menu = UI.menu("Windows")
      tools_menu = UI.menu("Tools")
      file_menu = UI.menu("File")

      # Create a variable to store our last tool used.
      @last_tool_name = ""

      # Create an array of timer ids that our tools create. That way we can clear
      # out any timers that are running if the user closes a model.
      @timer_ids = []

      # Create our toolbar and commands.
      toolbar = UI::Toolbar.new translate("Dynamic Components")

      # Create Interact Tool command.
      interact_tool_command = UI::Command.new(translate("Interact")) {
        myDCInteractTool = DCInteractTool.new(self)
        Sketchup.active_model.select_tool myDCInteractTool
      }

      interact_tool_command.large_icon =
        DynamicComponents::Util::get_image_file("interact_tool")
      interact_tool_command.small_icon =
        DynamicComponents::Util::get_image_file("interact_tool", "_small")

      interact_tool_command.tooltip = translate(
        "Trigger the interaction behavior of a dynamic component."
      )
      interact_tool_command.status_bar_text = interact_tool_command.tooltip
      toolbar.add_item interact_tool_command

      # Create Choose Options command.
      configurator_command = UI::Command.new($dc_CONFIGURATOR_NAME) {
        show_component_options()
      }

      configurator_command.large_icon =
        DynamicComponents::Util::get_image_file("configurator_tool")
      configurator_command.small_icon =
        DynamicComponents::Util::get_image_file("configurator_tool", "_small")

      configurator_command.tooltip = translate("View and configure the attributes of dynamic components.")
      configurator_command.status_bar_text = configurator_command.tooltip
      # configurator_command.set_validation_proc {
      #   if get_latest_class().configure_dialog_is_visible()
      #     MF_CHECKED
      #   else
      #     MF_ENABLED
      #   end
      # }
      toolbar.add_item configurator_command
      window_menu.add_item configurator_command
      tools_menu.add_item interact_tool_command

      # Create Generate Report command
      # For 2018 and beyond, we use the GenerateReport class.
      # For everything prior, we use the functions built into
      # dynamic components.
      post_2018_reporter_command = UI::Command.new($dc_REPORTER_NAME + "...") {
        if @generate_report && @generate_report.is_visible()
          @generate_report.close_dialog()
        else
          @generate_report = GenerateReport.new(@dc1)
          @generate_report.show_dialog()
        end
      }
      pre_2018_reporter_command = UI::Command.new($dc_REPORTER_NAME + "...") {
        if get_latest_class().reporter_dialog_is_visible()
          get_latest_class().close_reporter_dialog()
        else
          get_latest_class().show_reporter_dialog()
        end
      }

      # If we're offline, we use the shipped generate report html which
      # doesn't work correctly with the HTMLDialog, so fall back to the old path.
      if Sketchup.version.to_i < 18 || Sketchup.is_online == false
        @reporter_command = pre_2018_reporter_command
      else
        @reporter_command = post_2018_reporter_command
      end

      @reporter_command.set_validation_proc {
        if Sketchup.is_pro?
          @reporter_command.menu_text = $dc_REPORTER_NAME
        else
          @reporter_command.menu_text = $dc_REPORTER_NAME + "... " +
                                        translate("(Pro Only)")
        end

        if Sketchup.is_pro? == false
          MF_GRAYED
        elsif get_latest_class().reporter_dialog_is_visible()
          MF_CHECKED
        else
          MF_ENABLED
        end
      }

      reporter_description = translate("Export spreadsheet data from the model for take-offs or estimates.")
      if Sketchup.is_pro?
        @reporter_command.tooltip = reporter_description
        @reporter_command.menu_text = $dc_REPORTER_NAME + "..."
      else
        @reporter_command.tooltip = reporter_description + " " +
                                    translate("(Pro Only)")
        @reporter_command.menu_text = $dc_REPORTER_NAME + "... " +
                                      translate("(Pro Only)")
      end
      @reporter_command.status_bar_text = @reporter_command.tooltip

      file_menu.add_item @reporter_command
      # Create Manager command
      @manager_command = UI::Command.new($dc_MANAGER_NAME) {
        if get_latest_class().manager_dialog_is_visible()
          get_latest_class().close_manager_dialog()
        else

          # First look at our selection for a component.
          target_component = Sketchup.active_model.selection[0]

          # If we didn't find anything in our selection, look at the current
          # edit context.
          if target_component == nil && Sketchup.version.to_f >= 7.0
            active_path = Sketchup.active_model.active_path
            if active_path != nil
              entity = active_path.pop
              if entity.typename == "ComponentInstance"
                target_component = entity
              end
            end
          end

          if target_component == nil
            target_component = Sketchup.active_model
          end
          get_latest_class().show_manager_dialog(target_component)
        end
      }

      @manager_command.large_icon =
        DynamicComponents::Util::get_image_file("manager_tool")
      @manager_command.small_icon =
        DynamicComponents::Util::get_image_file("manager_tool", "_small")

      manager_description = translate("Create and customize the behaviors and data of dynamic components.")
      @manager_command.set_validation_proc {
        if Sketchup.is_pro?
          @manager_command.tooltip = manager_description
          @manager_command.menu_text = $dc_MANAGER_NAME
        else
          @manager_command.tooltip = manager_description + " " +
                                     translate("(Pro Only)")
          @manager_command.menu_text = $dc_MANAGER_NAME + " " +
                                       translate("(Pro Only)")
        end
        @manager_command.status_bar_text = @manager_command.tooltip
        if Sketchup.is_pro? == false
          MF_GRAYED
        elsif get_latest_class().manager_dialog_is_visible()
          MF_CHECKED
        else
          MF_ENABLED
        end
      }
      if Sketchup.is_pro?
        @manager_command.tooltip = manager_description
        @manager_command.menu_text = $dc_MANAGER_NAME
      else
        @manager_command.tooltip = manager_description + " " +
                                   translate("(Pro Only)")
        @manager_command.menu_text = $dc_MANAGER_NAME + " " +
                                     translate("(Pro Only)")
      end
      @manager_command.status_bar_text = @manager_command.tooltip
      toolbar.add_item @manager_command
      if get_latest_class().is_free? == false
        window_menu.add_item @manager_command
      end

      command = UI::Command.new("刷新amtf") { $AreloadDC.call() }
      command.small_icon = File.join($Aimages目录, "amtf-ml.png")
      command.large_icon = File.join($Aimages目录, "amtf-ml.png")
      command.tooltip = ("刷新amtf")
      command.status_bar_text = ("开发调试用……")
      toolbar.add_item(command)

      if toolbar.get_last_state == TB_NEVER_SHOWN
        # Hide toolbar at first startup
        toolbar.hide
      else
        # Show toolbar if it was open when we shutdown.
        toolbar.restore
        # Per bug 2902434, adding a timer call to restore the toolbar. This
        # fixes a toolbar resizing regression on PC as the restore() call
        # does not seem to work as the script is first loading.
        UI.start_timer(0.1, false) {
          toolbar.restore
        }
      end

      UI.add_context_menu_handler do |context_menu|
        selection = Sketchup.active_model.selection

        # Note that we used to check for components here via a call to a method
        # called selection_has_components. However, this was causing a very
        # strange bug (#1272217), so I took the code in the method and
        # placed it inline here. - Scott L.
        selection_has_components = false
        if selection.length == 0 || selection.length > MAX_SELECTION_LENGTH
          selection_has_components = false
        else
          for entity in selection
            if entity.typename == "ComponentInstance"
              selection_has_components = true
              break
            end
          end
        end

        if selection_has_components == true
          context_menu.add_separator()
          new_menu = context_menu.add_submenu(translate("Dynamic Components"))

          # When in component edit, one can open the context menu
          # without a selection, so only worry about the context menu if
          # we have a selection to respond against.
          if selection.length > 0
            new_menu.add_item(configurator_command)

            if get_latest_class().is_free? == false
              new_menu.add_item(@manager_command)
            end

            if Sketchup.is_pro? && selection.length == 1
              new_menu.add_separator()
              new_menu.add_item(translate("Swap Component")) {
                get_latest_class().swap_component(selection[0])
              }

              redraw_command = new_menu.add_item(translate("Redraw")) {
                get_class_by_version(selection[0]).redraw_with_undo(selection[0])
              }
              new_menu.set_validation_proc(redraw_command) {
                if get_class_by_version(selection[0]).has_behaviors(selection[0])
                  MF_ENABLED
                else
                  MF_GRAYED
                end
              }
            end
          end
        end
      end

      file_loaded(__FILE__)
    end #file_loaded
  end

  # Command handler method that shows the Component Options Dialog. Note that
  # this is being called by SketchUp's C++ code.
  #
  #   Args:
  #     None.
  #
  #   Returns:
  #     None.
  def show_component_options()
    puts "show_component_options   ……ing👇"
    if get_latest_class().configure_dialog_is_visible()
      get_latest_class().close_configure_dialog()
    else
      # When we activate the configure dialog, activate the selection tool
      # to avoid some of the changing and flashing that can occur as other
      # tools select things automatically.
      Sketchup.send_action "selectSelectionTool:"
      get_latest_class().show_configure_dialog()
    end
  end

  # Returns a reference to the latest dc class. This is used throughout
  # to know which version of the dc class to use by default.
  #
  #   Args:
  #     None.
  #
  #   Returns:
  #     reference: to the dynamic components class that is the default
  def get_latest_class()
    return @latest_class
  end

  # Returns a float representing an entity's dynamic components file format
  # version. This is based on polling an attribute called _formatversion.
  #
  # If no version is detected, then there are a couple of scenarios that might
  # be what's happening. If there is no version but it is a dynamic component,
  # then assume it was created with an old version of the plugin, and return
  # version 0.0. If it is not a dynamic component, then return the latest
  # version of the tool. (In other words, new dynamic components will always be
  # created using the latest version.)
  #
  #   Args:
  #     entity: reference to the entity we want to check for version
  #
  #   Returns:
  #     float: version number of the entity
  def get_version(entity)
    if entity.typename == "Group"
      attribute_entity = entity
    else
      attribute_entity = entity.definition
    end
    entity_version = attribute_entity.get_attribute("dynamic_attributes", "_formatversion")
    if entity_version == nil
      if attribute_entity.attribute_dictionary("dynamic_attributes") == nil

        # Assume that this is not a dynamic component, meaning we use the
        # latest version so we'll later deal with it using the latest class.
        entity_version = @latest_version

        # Then, check for a "3d warehouse" scenario where the actual dynamic
        # component is nested inside an otherwise empty shell
        if entity.typename == "ComponentInstance" && entity.name == ""
          subentity = entity.definition.entities[0]
          if (subentity.typename == "ComponentInstance" &&
              entity.definition.entities.length == 1)
            return get_version(subentity)
          end
        end
      else
        entity_version = 0.0
      end
    end
    return entity_version.to_f
  end

  # Returns a pointer to the dynamic components class instance that should be
  # used to handle a given entity. If the version does not match a version that
  # this plugin is aware of, then return nil.
  #
  #   Args:
  #     entity: reference to the entity we want to check for version
  #
  #   Returns:
  #     instance: reference to the dynamic components class that is appropriate
  def get_class_by_version(entity)
    version = get_version(entity)
    keys = @known_versions.keys
    for key in keys
      if key == version
        return @known_versions[key]
      end
    end
    # If we've come this far and there is not an explicit version set, then
    # use the latest one.
    return get_latest_class()
  end

  # Translate a string using the LangHandler object. Expects a global var
  # called $dc_strings (created in dynamiccomponents.rb)
  #
  #   Args:
  #      string_to_translate: UI string
  #
  #   Returns:
  #      string: nicely translated into the user's language by LangHandler
  def translate(string_to_translate)
    $dc_strings.GetString(string_to_translate)
  end

  # Compares the entity's format version to the version range that this
  # extension can support. If it falls outside that range, it shows an error
  # message and returns true.
  #
  #   Args:
  #     entity: reference to the entity we want to check for version
  #
  #   Returns:
  #     boolean: true if there is an error version
  def has_version_error(entity)
    entity_version = get_version(entity)

    if entity_version.to_f > @latest_version.to_f
      # TODO(scottlininger): we should provide a link to get the latest
      # installer or something like that. Also, should this version be
      # tied to SU? Might be less confusing.
      # TODO(scottlininger): Make this a dialog with a checkbox for
      # "Don't show this again." For now, I'll keep track of any DCs that
      # fire this message and not show it if a subsequent action is made.
      if @has_shown_version_error[entity.entityID.to_s] != true
        msg = "Error: This component was created with a newer version of the "
        msg += "Dynamic Components extension. Please visit sketchup.com "
        msg += "to upgrade."
        UI.messagebox(translate(msg))
        @has_shown_version_error[entity.entityID.to_s] = true
      end
      return true
    else
      return false
    end
  end

  # Records the last tool name.
  #
  #   Args:
  #     tool_name: string name of the last tool used
  #
  #   Returns:
  #     Nothing
  def last_tool_name=(tool_name)
    @last_tool_name = tool_name
  end

  # Returns the last tool name as stored by last_tool_name=
  #
  #   Args:
  #     none
  #
  #   Returns:
  #     string: last tool name
  def last_tool_name()
    return @last_tool_name
  end

  # Stores a list of all open timers so we can close them if a model is closed.
  #
  #   Args:
  #     timer_id  The timer id created in one of our tools.
  #
  #   Returns:
  #     nothing
  def register_timer_id(timer_id)
    @timer_ids.push timer_id
  end

  # Returns a list of all open timers.
  #
  #   Args:
  #     none
  #
  #   Returns:
  #     array of timer ids stored by register_timer_id
  def get_timer_ids
    return @timer_ids
  end

  ################################################################################
  # The DCToolsObserver class is the mechanism that is used to
  # watch the SketchUp tools and react accordingly to redraw dynamic components
  # as needed.
  class DCToolsObserver < Sketchup::ToolsObserver

    # Creates an instance of the observer. This method must be called with a
    # argument that identifies the current DynamicComponents instance.
    #
    #   Args:
    #     dynamic_components_instance  Instance of the main DC class
    def initialize(observers_instance)
      @started_tool = false
      @dcobservers = observers_instance
    end

    # Handler method that SketchUp will call with each tool activation.
    #
    #   Args:
    #     model     The SketchUp::Model that is active
    #     tool_name String of the tool name
    #     tool_id   String containing the tool id
    def onActiveToolChanged(model, tool_name, tool_id)
      # Store the active tool. This is used in our selection observer to ignore
      # the PushPullTool, MoveTool, and EraseTool, since those tools swap the
      # selection so quickly that it causes bugs if not captured.
      tool_name = fix_mac_tool_name(tool_name)
      @dcobservers.last_tool_name = tool_name
    end

    # Handler method that SketchUp will call with each tool interaction.
    #
    #   Args:
    #     model     The SketchUp::Model that is active
    #     tool_name String of the tool name
    #     tool_id   String containing the tool id
    #     state     Int containing the state of the tool
    def onToolStateChanged(model, tool_name, tool_id, state)
      tool_name = fix_mac_tool_name(tool_name)

      # Warn about change axes tool if we're trying to use it on a dynamic
      # component with behaviors.
      if tool_name == "ComponentCSTool"
        selection = Sketchup.active_model.selection
        if selection.length > 0
          entity = selection[0]
        else
          active_path = Sketchup.active_model.active_path
          entity = active_path[active_path.length - 1]
        end

        @dc = @dcobservers.get_class_by_version(entity)
        if @dc.has_behaviors(entity) &&
           @hide_change_axes_warning != true
          msg = @dc.translate("Warning: Changing a Dynamic Component's axes " +
                              "can have a serious effect on your model.") + "\n" +
                @dc.translate("Please be sure to save " +
                              "a copy of your model before proceeding with this change. " +
                              "Change axes now?")
          title = "Warning"
          response = UI.messagebox(msg, MB_OKCANCEL,
                                   @dc.translate(title))
          if response != 1 # 1 = Ok
            Sketchup.send_action "selectSelectionTool:"
          else
            # This means that the user has told us they know what they're
            # doing, so store that so we don't squawk at them again about
            # clicking on the 2nd point when changing their axes.
            @hide_change_axes_warning = true
          end
        end
      else
        # If any tool but the change axes tool is activated, then
        # reset our warning so that we'll show it again next time.
        # TODO(scottlininger): This is also a mechanism that we could use
        # to allow the user to say "never show this warning again", as soon
        # as we have a dialog for that from the ruby API.
        @hide_change_axes_warning = false
      end

      # Handle the ScaleTool or MoveTool if we've just *completed* an operation.
      if ((tool_name == "ScaleTool" || tool_name == "MoveTool") &&
          state == 0 && @started_tool == true)
        DCProgressBar.clear()

        if (@inside_observer_event)
          return
        end
        @inside_observer_event = true
        if tool_name == "MoveTool"
          undo_name = @dcobservers.translate("Move")
        else
          undo_name = @dcobservers.translate("Scale")
        end

        # Create our undo state, and append it to the previous scale or move.
        Sketchup.active_model.start_operation undo_name, true, false, true

        scaled_entities = []

        # Loop across the entities to explode any 3D warehouse models.
        for entity in Sketchup.active_model.selection
          if entity.typename == "ComponentInstance" ||
             entity.typename == "Group"
            scaled_entities.push(entity)
          end
        end
        for entity in scaled_entities
          if !entity.deleted?
            if entity.typename == "ComponentInstance" ||
               entity.typename == "Group"
              @dc = @dcobservers.get_class_by_version(entity)
              if @dc.has_behaviors(entity)
                @dc.explode_if_3d_warehouse(entity)
              end
            end
          end
        end

        # Loop across the entities to create a list that needs to be checked.
        scaled_entities = []
        for entity in Sketchup.active_model.selection
          if entity.typename == "ComponentInstance" ||
             entity.typename == "Group"
            scaled_entities.push(entity)
          end
        end

        # Keep track of whether we find anything that's a DC with behaviors.
        found_entity_to_redraw = false

        # Check each entity in our list for behaviors, and redraw if needed.
        for entity in scaled_entities
          if entity.to_s.index("Deleted") == nil
            if entity.typename == "ComponentInstance" ||
               entity.typename == "Group"
              if @dcobservers.has_version_error(entity) == false
                # We need to pull the version again, in case
                # explode_if_3d_warehouse above was fired
                @dc = @dcobservers.get_class_by_version(entity)
                if @dc.has_behaviors(entity)
                  if tool_name == "MoveTool"
                    if @dc.has_movetool_behaviors(entity)
                      found_entity_to_redraw = true
                      @dc.redraw(entity)
                    end
                  else
                    found_entity_to_redraw = true
                    @dc.redraw(entity)
                  end
                end
              end
            end
          end
        end

        # Update our configure and manage dialogs.
        if @dc
          @dc.refresh_dialogs()
        end

        # Make a "change" to the SketchUp model, so that our undo state gets
        # registered on the undo stack, even if we didn't make any changes
        # above. Then commit our operation.
        Sketchup.active_model.set_attribute "temp", "temp", 0
        Sketchup.active_model.delete_attribute "temp", "temp"
        Sketchup.active_model.commit_operation

        @started_tool = false
        @inside_observer_event = false
      elsif ((tool_name == "ScaleTool" || tool_name == "MoveTool") && state == 1)
        @started_tool = true
        #@dcobservers.get_latest_class().clear_highlight()
      end

      # Let's store our last tool so we can react properly to changes to the
      # scaletool attribute. (If the scale tool is active, then we want to
      # reselect it on a push_attribute of the scale_tool.)
      @dcobservers.last_tool_name = tool_name
    end

    # Returns the "correct" tool name to account for mac API differences.
    #
    #   Args:
    #     tool_name     The tool name to fix
    #
    #   Returns
    #     string   corrected tool name
    def fix_mac_tool_name(tool_name)
      if tool_name == "eTool"
        tool_name = "ScaleTool"
      elsif tool_name == "ool"
        tool_name = "MoveTool"
      elsif tool_name == "onentCSTool"
        tool_name = "ComponentCSTool"
      elsif tool_name == "PullTool"
        tool_name = "PushPullTool"
      end
      return tool_name
    end
  end

  #############################################################################
  # The DCSelectionObserver class is the mechanism that is used to detect what
  # the user is selecting and display appropriate changes in the configure
  # dialog.
  class DCSelectionObserver < Sketchup::SelectionObserver

    # Creates an instance of the observer. This method must be called with a
    # argument that identifies the current DynamicComponents instance.
    #
    #   Args:
    #     observers_instance  Instance of the main DCObservers class
    def initialize(observers_instance)
      @dcobservers = observers_instance
    end

    # Fires when the user makes changes to their selection.
    #
    #   Args:
    #     selection  The active selection
    def onSelectionBulkChange(selection)
      if @dcobservers.last_tool_name == "PushPullTool" ||
         @dcobservers.last_tool_name == "MoveTool" ||
         @dcobservers.last_tool_name == "EraseTool"
        return
      end
      @dcobservers.get_latest_class().refresh_dialogs(true)
    end

    # Fires when the user drags a new component into the model.
    #
    #   Args:
    #     selection  The active selection
    #     entity     The entity that was just added
    def onSelectionAdded(selection, entity)
      if @dcobservers.last_tool_name == "PushPullTool" ||
         @dcobservers.last_tool_name == "MoveTool" ||
         @dcobservers.last_tool_name == "EraseTool"
        return
      end
      @dcobservers.get_latest_class().refresh_dialogs(true)
    end

    # Fires when the user clears their selection entirely.
    #
    # TODO(scottlininger): The onclear fires before the onBulkChange, so if
    # the user is changing their selection, we fire end up clearing the
    # configure form and then updating it, which caused flashing. Need to
    # figure out a way to avoid that. Maybe with timers or some such.
    #
    #   Args:
    #     model  The active model
    def onSelectionCleared(selection)
      if @dcobservers.last_tool_name == "PushPullTool" ||
         @dcobservers.last_tool_name == "MoveTool" ||
         @dcobservers.last_tool_name == "EraseTool"
        return
      end
      @dcobservers.get_latest_class().refresh_dialogs(true)
    end
  end

  #############################################################################
  # The DCDefinitionsObserver class is the mechanism that is used to detect
  # when the user creates a new definition.
  class DCDefinitionsObserver < Sketchup::DefinitionsObserver

    # Creates an instance of the observer. This method must be called with a
    # argument that identifies the current DynamicComponents instance.
    #
    #   Args:
    #     observers_instance  Instance of the main DCObservers class
    def initialize(observers_instance)
      @dcobservers = observers_instance
    end

    # Captures each new definition as it is added, and determines whether the
    # green DC badge should be shown. This is different from onPlaceComponent,
    # because onPlaceComponent does NOT fire when someone groups items together
    # and creates a new component from scratch.
    #
    #   Args:
    #     definitions  Definitions collection that is being added to
    #     definition  Definition that was added.
    def onComponentAdded(definitions, definition)
      if definition.valid? && definition.count_instances > 0
        dc = @dcobservers.get_latest_class()
        dc.has_behaviors(definition.instances[0])
      end
    end
  end

  #############################################################################
  # The DCAppObserver class is the mechanism that is used to
  # attach the other observers whenever a new model is opened in SketchUp.
  class DCModelObserver < Sketchup::ModelObserver

    # Creates an instance of the observer. This method must be called with a
    # argument that identifies the current DynamicComponents instance.
    #
    #   Args:
    #     observers_instance  Instance of the main DCObservers class
    def initialize(observers_instance)
      @dcobservers = observers_instance
      @last_active_path = []

      # Create some global hashes to store our cached edit transforms into.
      # We have one that contains transformations relative to the global axes
      # and one that contains transformations relative to the local axes.
      #
      # TODO(scottlininger): Jon Ulmer suggested that he could expose these as
      # a standard part of the API, so I'm okay leaving these as global
      # variables for the moment until we decide whether that's worth doing. In
      # any case, we should at least change these from globals to privates
      # before launch. I didn't do that at this stage because it would require
      # some refactoring to create a global variable space that all of our
      # future DC versions could get to.
      $local_edit_transforms = {}
      $global_edit_transforms = {}

      identity_vector = Geom::Vector3d.new 0.0, 0.0, 0.0
      @identity_transform = Geom::Transformation.translation identity_vector
    end

    # Checks what's happening as the user enters and exits edit mode. If it
    # detects an exit, it calls set_last_size on the entity to update that
    # information. This fixes a bug where manually adding or removing geometry
    # would get the last_size out of sync and lead to strange scaling behaviors
    # in those cases.
    #
    #   Args:
    #     observers_instance  Instance of the main DCObservers class
    def onActivePathChanged(model)
      if @last_edit_transform == nil
        @last_edit_transform = @identity_transform
      end

      if model.active_path == nil
        active_path_length = 0
      else
        active_path_length = model.active_path.length
      end
      if @last_active_path == nil
        last_path_length = 0
      else
        last_path_length = @last_active_path.length
      end

      if last_path_length > active_path_length
        entity = @last_active_path.pop

        # SU-43238 throwing error on attributes of deleted entities.
        if entity.deleted?
          @last_active_path = model.active_path
          return
        end

        # If the manager dialog is open, then select the component that we're
        # leaving edit mode from.
        if @dcobservers.get_latest_class().manager_dialog_is_visible
          Sketchup.active_model.selection.add(entity)
        end

        # Since we're leaving edit mode, clear out any cached transforms.
        $local_edit_transforms[entity.to_s] = nil
        $global_edit_transforms[entity.to_s] = nil
        @last_edit_transform = @identity_transform

        # Only bother storing a new size if the old size was previously set.
        last_lenx = entity.get_attribute @dictionary_name, "_last_lenx"
        if last_lenx != nil
          # Store the new size of our component inside an operation to
          # avoid extra undo states showing up.
          lenx, leny, lenz = entity.scaled_size
          Sketchup.active_model.start_operation "Close", true, false, true
          entity.set_last_size(lenx, leny, lenz)
          Sketchup.active_model.commit_operation
        end
      elsif last_path_length < active_path_length
        entity = model.active_path.last

        # Since we're entering a deeper level of edit mode, cache the edit
        # transform so we can use it to correctly calculate the local
        # transforms of a given element. (See dcclass_v1.rb for the
        # edit_transform method)
        $local_edit_transforms[entity.to_s] = @last_edit_transform.inverse *
                                              model.edit_transform
        $global_edit_transforms[entity.to_s] = model.edit_transform
        @last_edit_transform = model.edit_transform
      end

      @last_active_path = model.active_path
    end

    # If the user selects "undo" or "redo" refresh our dialogs to match.
    def onTransactionUndo(model)
      @dcobservers.get_latest_class().refresh_dialogs()
    end

    def onTransactionRedo(model)
      @dcobservers.get_latest_class().refresh_dialogs()
    end

    # If a classification has changed in the model, refresh our dialogs.
    def onClassificationChanged(model)
      @dcobservers.get_latest_class().refresh_dialogs()
    end

    # If the user performs a right click > Save As, store any instance-level
    # attributes onto the definition so they'll be there when the .skp file
    # is later imported into a new model.
    def onBeforeComponentSaveAs(instance)
      @dcobservers.get_latest_class().store_instance_cache(instance)
    end

    def onAfterComponentSaveAs(instance)
      @dcobservers.get_latest_class().clear_instance_cache(instance)
    end

    # As the user drags in a component from the component browser or from a
    # file > import, this callback will fire so we can handle DC placement.
    def onPlaceComponent(instance)
      return unless instance.valid?
      dc = @dcobservers.get_class_by_version(instance)
      dc.handle_place_component(instance)
    end

    # When you close out a model, this observer is fired. Here is where we
    # refresh our dialogs and clear out any running timers (from animations.)
    #
    #   Args:
    #     model  The active model
    def onDeleteModel(model)
      dc = @dcobservers.get_latest_class
      dc.refresh_dialogs()
      for timer_id in @dcobservers.get_timer_ids
        UI.stop_timer(timer_id)
      end
    end
  end

  #############################################################################
  # The DCAppObserver class is the mechanism that is used to
  # attach the other observers whenever a new model is opened in SketchUp.
  class DCAppObserver < Sketchup::AppObserver

    # Creates an instance of the observer. This method must be called with a
    # argument that identifies the current DynamicComponents instance.
    #
    #   Args:
    #     dynamic_components_instance  Instance of the main DC class
    def initialize(observers_instance)
      @dcobservers = observers_instance
      begin
        attach_observers(Sketchup.active_model)
      rescue
        # must be a mac
      end
    end

    # Attaches other observers whenever a new model is created
    #
    #   Args:
    #     model  The active model
    def onNewModel(model)
      attach_observers(model)
    end

    # Attaches other observers whenever a model is opened
    #
    #   Args:
    #     model  The active model
    def onOpenModel(model)
      attach_observers(model)
    end

    # Attaches all observers.
    #
    #   Args:
    #     model  the model to attach observers to
    def attach_observers(model)
      $ADCToolsObserver = DCObservers::DCToolsObserver.new(@dcobservers)
      model.tools.add_observer(
        $ADCToolsObserver
      )
      $ADCSelectionObserver = DCObservers::DCSelectionObserver.new(@dcobservers)

      model.selection.add_observer(
        $ADCSelectionObserver
      )

      $ADCModelObserver = DCModelObserver.new(@dcobservers)

      model.add_observer(
        $ADCModelObserver
      )

      $ADCDefinitionsObserver = DCDefinitionsObserver.new(@dcobservers)
      model.definitions.add_observer(
        $ADCDefinitionsObserver
      )
    end

    # Shows a warning message if the end user deactivates an extension.
    #
    #   Args:
    #     data  String describing the disabled extension.
    def onUnloadExtension(data)
      msg = "WARNING. You have chosen to disable the Dynamic Components " +
            "Extension. Editing a Dynamic Component manually while the " +
            "extension is disabled could lead to strange behaviors when " +
            "the Extension is later reactivated. " +
            "Exploding such Components before editing them is advised."
      msg = @dcobservers.translate(msg)
      if data.to_s == @dcobservers.translate("Dynamic Components") ||
         data.to_s == "Dynamic Components"
        UI.messagebox(msg)
      end
    end
  end
end

$dc_observers = DCObservers.new()
